package cn.edu.thu.laud.com.utils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.Socket;
import java.util.Random;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;


public class IOUtils {

	/**
	 * 
	 * Random number generator to make unique file name
	 */
	private final static Random RANDOM_GEN = new Random(
			System.currentTimeMillis());

	/***
	 * The default buffer size used by {@link #copyStream copyStream } and
	 * {@link #copyReader copyReader }. It's value is 1024.
	 ***/
	public static final int DEFAULT_COPY_BUFFER_SIZE = 1024;

	/**
	 * Get a <code>BufferedInputStream</code>.
	 */
	public final static BufferedInputStream getBufferedInputStream(
			InputStream in) {
		BufferedInputStream bin = null;
		if (in instanceof java.io.BufferedInputStream) {
			bin = (BufferedInputStream) in;
		} else {
			bin = new BufferedInputStream(in);
		}
		return bin;
	}

	/**
	 * Get a <code>BufferedOutputStream</code>.
	 */
	public final static BufferedOutputStream getBufferedOutputStream(
			OutputStream out) {
		BufferedOutputStream bout = null;
		if (out instanceof java.io.BufferedOutputStream) {
			bout = (BufferedOutputStream) out;
		} else {
			bout = new BufferedOutputStream(out);
		}
		return bout;
	}

	public final static InflaterInputStream getInflaterInputStream(
			InputStream in) {
		InflaterInputStream bin = null;
		if (in instanceof InflaterInputStream) {
			bin = (InflaterInputStream) in;
		} else {
			bin = new InflaterInputStream(in);
		}
		return bin;
	}

	public final static DeflaterOutputStream getDeflaterOutputStream(
			OutputStream out) {
		DeflaterOutputStream bout = null;
		if (out instanceof DeflaterOutputStream) {
			bout = (DeflaterOutputStream) out;
		} else {
			bout = new DeflaterOutputStream(out);
		}
		return bout;
	}

	/**
	 * Get <code>BufferedReader</code>.
	 */
	public final static BufferedReader getBufferedReader(Reader reader) {
		BufferedReader buffered = null;
		if (reader instanceof java.io.BufferedReader) {
			buffered = (BufferedReader) reader;
		} else {
			buffered = new BufferedReader(reader);
		}
		return buffered;
	}

	/**
	 * Get <code>BufferedWriter</code>.
	 */
	public final static BufferedWriter getBufferedWriter(Writer wr) {
		BufferedWriter bw = null;
		if (wr instanceof java.io.BufferedWriter) {
			bw = (BufferedWriter) wr;
		} else {
			bw = new BufferedWriter(wr);
		}
		return bw;
	}

	/**
	 * Get unique file object.
	 */
	public final static File getUniqueFile(File oldFile) {
		File newFile = oldFile;
		while (true) {
			if (!newFile.exists()) {
				break;
			}
			newFile = new File(oldFile.getAbsolutePath() + '.'
					+ Math.abs(RANDOM_GEN.nextLong()));
		}
		return newFile;
	}

	/**
	 * No exception <code>InputStream</code> close method.
	 */
	public final static void close(InputStream is) {
		if (is != null) {
			try {
				is.close();
			} catch (Exception ex) {
			}
		}
	}

	/**
	 * No exception <code>OutputStream</code> close method.
	 */
	public final static void close(OutputStream os) {
		if (os != null) {
			try {
				os.close();
			} catch (Exception ex) {
			}
		}
	}

	/**
	 * Closes the socket quietly, catching rather than throwing IOException.
	 * Intended for use from finally blocks.
	 * 
	 * @param socket
	 *            the socket to close, may be {@code null}
	 * @since 3.0
	 */
	public static void close(Socket socket) {
		if (socket != null) {
			try {
				socket.close();
			} catch (IOException e) {
			}
		}
	}

	/**
	 * No exception <code>java.io.Reader</code> close method.
	 */
	public final static void close(Reader rd) {
		if (rd != null) {
			try {
				rd.close();
			} catch (Exception ex) {
			}
		}
	}

	/**
	 * No exception <code>java.io.Writer</code> close method.
	 */
	public final static void close(Writer wr) {
		if (wr != null) {
			try {
				wr.close();
			} catch (Exception ex) {
			}
		}
	}

	/**
	 * Get exception stack trace.
	 */
	public final static String getStackTrace(Throwable ex) {
		String result = "";
		if (ex != null) {
			try {
				StringWriter sw = new StringWriter();
				PrintWriter pw = new PrintWriter(sw);
				ex.printStackTrace(pw);
				pw.close();
				sw.close();
				result = sw.toString();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return result;
	}

	/**
	 * Copy chars from a <code>Reader</code> to a <code>Writer</code>.
	 * 
	 * @param bufferSize
	 *            Size of internal buffer to use.
	 */
	public final static void copy(Reader input, Writer output, int bufferSize)
			throws IOException {
		char buffer[] = new char[bufferSize];
		int n = 0;
		while ((n = input.read(buffer)) != -1) {
			output.write(buffer, 0, n);
		}
	}

	/**
	 * Copy chars from a <code>InputStream</code> to a <code>OutputStream</code>
	 * .
	 * 
	 * @param bufferSize
	 *            Size of internal buffer to use.
	 */
	public final static void copy(InputStream input, OutputStream output,
			int bufferSize) throws IOException {
		byte buffer[] = new byte[bufferSize];
		int n = 0;
		while ((n = input.read(buffer)) != -1) {
			output.write(buffer, 0, n);
		}
	}

	/**
	 * Read fully from reader
	 */
	public final static String readFully(Reader reader) throws IOException {
		StringWriter writer = new StringWriter();
		copy(reader, writer, 1024);
		return writer.toString();
	}

	/**
	 * Read fully from stream
	 */
	public final static String readFully(InputStream input) throws IOException {
		StringWriter writer = new StringWriter();
		InputStreamReader reader = new InputStreamReader(input);
		copy(reader, writer, 1024);
		return writer.toString();
	}

	public final static void delete(File file) throws IOException {
		if (!file.exists()) {
			return;
		} else if (file.isDirectory()) {
			deleteDir(file);
		} else {
			deleteFile(file);
		}
	}

	private final static void deleteDir(File dir) throws IOException {
		File[] children = dir.listFiles();

		if (children == null) {
			return;
		}

		for (int i = 0; i < children.length; i++) {
			File file = children[i];
			delete(file);
		}

		if (!dir.delete()) {
			throw new IOException("Failed to delete directory: " + dir);
		}

	}

	private final static void deleteFile(File file) throws IOException {
		if (!file.delete()) {
			// hack around bug where files will sometimes not be deleted on
			// Windows
			if (OS.isFamilyWindows()) {
				System.gc();
			}
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
			}
			if (!file.delete()) {
				throw new IOException("Failed to delete file: " + file);
			}
		}
	}

	/***
	 * Copies the contents of an InputStream to an OutputStream using a copy
	 * buffer of a given size and notifies the provided CopyStreamListener of
	 * the progress of the copy operation by calling its bytesTransferred(long,
	 * int) method after each write to the destination. If you wish to notify
	 * more than one listener you should use a CopyStreamAdapter as the listener
	 * and register the additional listeners with the CopyStreamAdapter.
	 * <p>
	 * The contents of the InputStream are read until the end of the stream is
	 * reached, but neither the source nor the destination are closed. You must
	 * do this yourself outside of the method call. The number of bytes
	 * read/written is returned.
	 * <p>
	 * 
	 * @param source
	 *            The source InputStream.
	 * @param dest
	 *            The destination OutputStream.
	 * @param bufferSize
	 *            The number of bytes to buffer during the copy.
	 * @param streamSize
	 *            The number of bytes in the stream being copied. Should be set
	 *            to CopyStreamEvent.UNKNOWN_STREAM_SIZE if unknown.
	 * @param listener
	 *            The CopyStreamListener to notify of progress. If this
	 *            parameter is null, notification is not attempted.
	 * @param flush
	 *            Whether to flush the output stream after every write. This is
	 *            necessary for interactive sessions that rely on buffered
	 *            streams. If you don't flush, the data will stay in the stream
	 *            buffer.
	 * @exception CopyStreamException
	 *                If an error occurs while reading from the source or
	 *                writing to the destination. The CopyStreamException will
	 *                contain the number of bytes confirmed to have been
	 *                transferred before an IOException occurred, and it will
	 *                also contain the IOException that caused the error. These
	 *                values can be retrieved with the CopyStreamException
	 *                getTotalBytesTransferred() and getIOException() methods.
	 ***/
	public static final long copyStream(InputStream source, OutputStream dest,
			int bufferSize, long streamSize, CopyStreamListener listener,
			boolean flush) throws CopyStreamException {
		//TODO this is just a bookmark
		int bytes = 0;
		long total;
		byte[] buffer;

		buffer = new byte[bufferSize];
		total = 0;

		if (streamSize == CopyStreamEvent.UNKNOWN_STREAM_SIZE)
			streamSize = Long.MAX_VALUE;
		try {
			if(streamSize==0){
				return 0;
			}
			while (total < streamSize
					&& (bytes = source.read(buffer, 0, (int) (streamSize
							- total > buffer.length ? buffer.length
							: streamSize - total))) != -1) {

				// Technically, some read(byte[]) methods may return 0 and we
				// cannot
				// accept that as an indication of EOF.
				// if (total >= streamSize * 0.8)
				// System.out.println("test");
				//System.out.println("process:"
				//		+ (float) (total / (double) streamSize * 100));
				//System.out.print(".");
				if (bytes == 0) {
					bytes = source.read();
					if (bytes < 0)
						break;
					dest.write(bytes);
					if (flush){
						dest.flush();
						
					}
					++total;

					if (listener != null)
						listener.bytesTransferred(total, 1, streamSize);
					continue;
				}

				dest.write(buffer, 0, bytes);
				if (flush){
						dest.flush();
				}
				total += bytes;
				if (listener != null)
					listener.bytesTransferred(total, bytes, streamSize);

			}
		} catch (IOException e) {
			e.printStackTrace();
			throw new CopyStreamException("IOException caught while copying.",
					total, e);
		}
		System.out.println("done");
		return total;
	}

	/***
	 * Copies the contents of an InputStream to an OutputStream using a copy
	 * buffer of a given size and notifies the provided CopyStreamListener of
	 * the progress of the copy operation by calling its bytesTransferred(long,
	 * int) method after each write to the destination. If you wish to notify
	 * more than one listener you should use a CopyStreamAdapter as the listener
	 * and register the additional listeners with the CopyStreamAdapter.
	 * <p>
	 * The contents of the InputStream are read until the end of the stream is
	 * reached, but neither the source nor the destination are closed. You must
	 * do this yourself outside of the method call. The number of bytes
	 * read/written is returned.
	 * <p>
	 * 
	 * @param source
	 *            The source InputStream.
	 * @param dest
	 *            The destination OutputStream.
	 * @param bufferSize
	 *            The number of bytes to buffer during the copy.
	 * @param streamSize
	 *            The number of bytes in the stream being copied. Should be set
	 *            to CopyStreamEvent.UNKNOWN_STREAM_SIZE if unknown.
	 * @param listener
	 *            The CopyStreamListener to notify of progress. If this
	 *            parameter is null, notification is not attempted.
	 * @exception CopyStreamException
	 *                If an error occurs while reading from the source or
	 *                writing to the destination. The CopyStreamException will
	 *                contain the number of bytes confirmed to have been
	 *                transferred before an IOException occurred, and it will
	 *                also contain the IOException that caused the error. These
	 *                values can be retrieved with the CopyStreamException
	 *                getTotalBytesTransferred() and getIOException() methods.
	 ***/
	public static final long copyStream(InputStream source, OutputStream dest,
			int bufferSize, long streamSize, CopyStreamListener listener)
			throws CopyStreamException {
		return copyStream(source, dest, bufferSize, streamSize, listener, true);
	}

	/***
	 * Copies the contents of an InputStream to an OutputStream using a copy
	 * buffer of a given size. The contents of the InputStream are read until
	 * the end of the stream is reached, but neither the source nor the
	 * destination are closed. You must do this yourself outside of the method
	 * call. The number of bytes read/written is returned.
	 * <p>
	 * 
	 * @param source
	 *            The source InputStream.
	 * @param dest
	 *            The destination OutputStream.
	 * @return The number of bytes read/written in the copy operation.
	 * @exception CopyStreamException
	 *                If an error occurs while reading from the source or
	 *                writing to the destination. The CopyStreamException will
	 *                contain the number of bytes confirmed to have been
	 *                transferred before an IOException occurred, and it will
	 *                also contain the IOException that caused the error. These
	 *                values can be retrieved with the CopyStreamException
	 *                getTotalBytesTransferred() and getIOException() methods.
	 ***/
	public static final long copyStream(InputStream source, OutputStream dest,
			int bufferSize) throws CopyStreamException {
		return copyStream(source, dest, bufferSize,
				CopyStreamEvent.UNKNOWN_STREAM_SIZE, null);
	}

	/***
	 * Same as
	 * <code> copyStream(source, dest, DEFAULT_COPY_BUFFER_SIZE); </code>
	 ***/
	public static final long copyStream(InputStream source, OutputStream dest)
			throws CopyStreamException {
		return copyStream(source, dest, DEFAULT_COPY_BUFFER_SIZE);
	}

	/***
	 * Copies the contents of a Reader to a Writer using a copy buffer of a
	 * given size and notifies the provided CopyStreamListener of the progress
	 * of the copy operation by calling its bytesTransferred(long, int) method
	 * after each write to the destination. If you wish to notify more than one
	 * listener you should use a CopyStreamAdapter as the listener and register
	 * the additional listeners with the CopyStreamAdapter.
	 * <p>
	 * The contents of the Reader are read until its end is reached, but neither
	 * the source nor the destination are closed. You must do this yourself
	 * outside of the method call. The number of characters read/written is
	 * returned.
	 * <p>
	 * 
	 * @param source
	 *            The source Reader.
	 * @param dest
	 *            The destination writer.
	 * @param bufferSize
	 *            The number of characters to buffer during the copy.
	 * @param streamSize
	 *            The number of characters in the stream being copied. Should be
	 *            set to CopyStreamEvent.UNKNOWN_STREAM_SIZE if unknown.
	 * @param listener
	 *            The CopyStreamListener to notify of progress. If this
	 *            parameter is null, notification is not attempted.
	 * @return The number of characters read/written in the copy operation.
	 * @exception CopyStreamException
	 *                If an error occurs while reading from the source or
	 *                writing to the destination. The CopyStreamException will
	 *                contain the number of bytes confirmed to have been
	 *                transferred before an IOException occurred, and it will
	 *                also contain the IOException that caused the error. These
	 *                values can be retrieved with the CopyStreamException
	 *                getTotalBytesTransferred() and getIOException() methods.
	 ***/
	public static final long copyReader(Reader source, Writer dest,
			int bufferSize, long streamSize, CopyStreamListener listener)
			throws CopyStreamException {
		int chars;
		long total;
		char[] buffer;

		buffer = new char[bufferSize];
		total = 0;

		try {
			while ((chars = source.read(buffer)) != -1) {
				// Technically, some read(char[]) methods may return 0 and we
				// cannot
				// accept that as an indication of EOF.
				if (chars == 0) {
					chars = source.read();
					if (chars < 0)
						break;
					dest.write(chars);
					dest.flush();
					++total;
					if (listener != null)
						listener.bytesTransferred(total, chars, streamSize);
					continue;
				}

				dest.write(buffer, 0, chars);
				dest.flush();
				total += chars;
				if (listener != null)
					listener.bytesTransferred(total, chars, streamSize);
			}
		} catch (IOException e) {
			throw new CopyStreamException("IOException caught while copying.",
					total, e);
		}

		return total;
	}

	/***
	 * Copies the contents of a Reader to a Writer using a copy buffer of a
	 * given size. The contents of the Reader are read until its end is reached,
	 * but neither the source nor the destination are closed. You must do this
	 * yourself outside of the method call. The number of characters
	 * read/written is returned.
	 * <p>
	 * 
	 * @param source
	 *            The source Reader.
	 * @param dest
	 *            The destination writer.
	 * @param bufferSize
	 *            The number of characters to buffer during the copy.
	 * @return The number of characters read/written in the copy operation.
	 * @exception CopyStreamException
	 *                If an error occurs while reading from the source or
	 *                writing to the destination. The CopyStreamException will
	 *                contain the number of bytes confirmed to have been
	 *                transferred before an IOException occurred, and it will
	 *                also contain the IOException that caused the error. These
	 *                values can be retrieved with the CopyStreamException
	 *                getTotalBytesTransferred() and getIOException() methods.
	 ***/
	public static final long copyReader(Reader source, Writer dest,
			int bufferSize) throws CopyStreamException {
		return copyReader(source, dest, bufferSize,
				CopyStreamEvent.UNKNOWN_STREAM_SIZE, null);
	}

	/***
	 * Same as
	 * <code> copyReader(source, dest, DEFAULT_COPY_BUFFER_SIZE); </code>
	 ***/
	public static final long copyReader(Reader source, Writer dest)
			throws CopyStreamException {
		return copyReader(source, dest, DEFAULT_COPY_BUFFER_SIZE);
	}

}
